﻿using System;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.Owin.Security;
using SDCloudSolution.SDCloud.BusinessLayer.Identity;
using SDCloudSolution.SDCloud.BusinessLayer.Interfaces;
using SDCloudSolution.SDCloud.Entities.Identity;
using SDCloudSolution.SDCloud.Web.Models;
using SDCloudSolution.SDCloud.Web.Models.Account;

namespace SDCloudSolution.SDCloud.Web.Controllers
{
    [Authorize]
    [RoutePrefix("account")]
    public partial class AccountController : Controller
    {
        private readonly ApplicationUserManager _userManager;

        private readonly IClientApplicationComponent _clientApplicationComponent;

        public AccountController(ApplicationUserManager userManager, IClientApplicationComponent clientApplicationComponent)
        {
            _userManager = userManager;
            _clientApplicationComponent = clientApplicationComponent;
        }


        [Route("login")]
        [HttpGet]
        [AllowAnonymous]
        public virtual ActionResult Login(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        [Route("login")]
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Login(LoginViewModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                var user = _userManager.Find(model.UserName, model.Password);
                if (user != null)
                {
                    SignIn(user, model.RememberMe);
                    return RedirectToLocal(returnUrl);
                }

                ModelState.AddModelError("", "Invalid username or password.");
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        [Route("register")]
        [HttpGet]
        [AllowAnonymous]
        public virtual ActionResult Register()
        {
            return View();
        }

        [Route("register")]
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> Register(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUserEntity
                {
                    UserName = model.UserName,
                    Email = model.Email,
                    RegistrationDateUtc = DateTime.UtcNow
                };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    SignIn(user, isPersistent: false);
                    return RedirectToAction(MVC.Dashboard.Index());
                }
                else
                {
                    AddErrors(result);
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }


        [Route("disassociate")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> Disassociate(string loginProvider, string providerKey)
        {
            ManageMessageId? message = null;
            IdentityResult result = await _userManager.RemoveLoginAsync(User.Identity.GetUserId<int>(), new UserLoginInfo(loginProvider, providerKey));
            if (result.Succeeded)
            {
                message = ManageMessageId.RemoveLoginSuccess;
            }
            else
            {
                message = ManageMessageId.Error;
            }

            return RedirectToAction(MVC.Account.Manage(message));
        }

        [Route("manage")]
        [HttpGet]
        public virtual async Task<ActionResult> Manage(ManageMessageId? message)
        {
            var user = await _userManager.FindByIdAsync(User.Identity.GetUserId<int>());

            var model = new ManagePageViewModel
            {
                HasLocalPassword = HasPassword(),
                User = user,
                PasswordManageViewModel = new ManageUserViewModel()
            };

            ViewBag.StatusMessage =
                message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed."
                : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set."
                : message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed."
                : message == ManageMessageId.Error ? "An error has occurred."
                : "";
            ViewBag.ReturnUrl = Url.Action(MVC.Account.Manage());

            return View(model);
        }

        [Route("manage")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> Manage(ManageUserViewModel model)
        {
            bool hasPassword = HasPassword();
            ViewBag.ReturnUrl = MVC.Account.Manage();
            if (hasPassword)
            {
                if (ModelState.IsValid)
                {
                    IdentityResult result = await _userManager.ChangePasswordAsync(User.Identity.GetUserId<int>(), model.OldPassword, model.NewPassword);
                    if (result.Succeeded)
                    {
                        return RedirectToAction(MVC.Account.Manage(ManageMessageId.ChangePasswordSuccess));
                    }
                    else
                    {
                        AddErrors(result);
                    }
                }
            }
            else
            {
                // User does not have a password so remove any validation errors caused by a missing OldPassword field
                ModelState state = ModelState["OldPassword"];
                if (state != null)
                {
                    state.Errors.Clear();
                }

                if (ModelState.IsValid)
                {
                    IdentityResult result = await _userManager.AddPasswordAsync(User.Identity.GetUserId<int>(), model.NewPassword);
                    if (result.Succeeded)
                    {
                        return RedirectToAction(MVC.Account.Manage(ManageMessageId.SetPasswordSuccess));
                    }
                    else
                    {
                        AddErrors(result);
                    }
                }
            }

            // If we got this far, something failed, redisplay form
            var pageModel = new ManagePageViewModel
            {
                HasLocalPassword = hasPassword,
                User = await _userManager.FindByIdAsync(User.Identity.GetUserId<int>()),
                PasswordManageViewModel = model
            };

            return View(pageModel);
        }

        [Route("externallogin")]
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual ActionResult ExternalLogin(string provider, string returnUrl)
        {
            // Request a redirect to the external login provider
            return new ChallengeResult(provider, Url.Action(MVC.Account.ExternalLoginCallback(returnUrl)));
        }

        [Route("externallogincallback")]
        [HttpGet]
        [AllowAnonymous]
        public virtual async Task<ActionResult> ExternalLoginCallback(string returnUrl)
        {
            var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
            if (loginInfo == null)
            {
                return RedirectToAction(MVC.Account.Login());
            }

            // Sign in the user with this external login provider if the user already has a login
            var user = await _userManager.FindAsync(loginInfo.Login);
            if (user != null)
            {
                SignIn(user, isPersistent: false);
                return RedirectToLocal(returnUrl);
            }
            else
            {
                // If the user does not have an account, then prompt the user to create an account
                ViewBag.ReturnUrl = returnUrl;
                ViewBag.LoginProvider = loginInfo.Login.LoginProvider;

                var model = new ExternalLoginConfirmationViewModel
                {
                    UserName = loginInfo.DefaultUserName
                };

                return View(MVC.Account.Views.ExternalLoginConfirmation, model);
            }
        }

        [Route("linklogin")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public virtual ActionResult LinkLogin(string provider)
        {
            // Request a redirect to the external login provider to link a login for the current user
            return new ChallengeResult(provider, Url.Action(MVC.Account.LinkLoginCallback()), User.Identity.GetUserId());
        }

        [Route("linklogincallback")]
        [HttpGet]
        public virtual async Task<ActionResult> LinkLoginCallback()
        {
            var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId());
            if (loginInfo == null)
            {
                return RedirectToAction(MVC.Account.Manage(ManageMessageId.Error));
            }
            var result = await _userManager.AddLoginAsync(User.Identity.GetUserId<int>(), loginInfo.Login);
            if (result.Succeeded)
            {
                return RedirectToAction(MVC.Account.Manage());
            }

            return RedirectToAction(MVC.Account.Manage(ManageMessageId.Error));
        }

        [Route("externalloginconfirmation")]
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
        {
            if (User.Identity.IsAuthenticated)
            {
                return RedirectToAction(MVC.Account.Manage());
            }

            if (ModelState.IsValid)
            {
                // Get the information about the user from the external login provider
                var info = await AuthenticationManager.GetExternalLoginInfoAsync();
                if (info == null)
                {
                    return RedirectToAction(MVC.Account.ExternalLoginFailure());
                }
                var user = new ApplicationUserEntity
                {
                    UserName = model.UserName
                };

                var result = await _userManager.CreateAsync(user);

                if (result.Succeeded)
                {
                    result = await _userManager.AddLoginAsync(user.Id, info.Login);
                    if (result.Succeeded)
                    {
                        SignIn(user, isPersistent: false);
                        return RedirectToLocal(returnUrl);
                    }
                }

                AddErrors(result);
            }

            ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }

        [Route("logoff")]
        [HttpPost]
        [ValidateAntiForgeryToken]
        public virtual ActionResult LogOff()
        {
            AuthenticationManager.SignOut();
            return RedirectToAction(MVC.Home.Index());
        }

        [Route("externalloginfailure")]
        [HttpGet]
        [AllowAnonymous]
        public virtual ActionResult ExternalLoginFailure()
        {
            return View();
        }

        [ChildActionOnly]
        public virtual ActionResult UserClientApplicationList()
        {
            var clientApplications = _clientApplicationComponent.GetByUserId(User.Identity.GetUserId<int>());

            var model = new ManageUserClientApplications
            {
                Applications = clientApplications.Select(application => new ClientApplicationViewModel
                {
                    ClientId = application.ClientId,
                    ClientSecret = application.ClientSecret,
                    IsActive = application.IsActive,
                    IsAutoGenerated = application.IsAutoGenerated,
                    Name = application.Name
                }).ToList()
            };

            return PartialView(MVC.Account.Views._ClientApplicationList, model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public virtual ActionResult GenerateClientCredentials()
        {
            _clientApplicationComponent.GenerateApplicationForUser(User.Identity.GetUserId<int>());

            return Redirect(Url.Action(MVC.Account.Manage()));
        }

        [ChildActionOnly]
        public virtual ActionResult RemoveAccountList()
        {
            var linkedAccounts = _userManager.GetLogins(User.Identity.GetUserId<int>());
            ViewBag.ShowRemoveButton = HasPassword() || linkedAccounts.Count > 1;
            return (ActionResult)PartialView(MVC.Account.Views._RemoveAccountPartial, linkedAccounts);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && _userManager != null)
            {
                _userManager.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Helpers
        // Used for XSRF protection when adding external logins
        private const string XsrfKey = "XsrfId";

        private IAuthenticationManager AuthenticationManager
        {
            get
            {
                return HttpContext.GetOwinContext().Authentication;
            }
        }

        private void SignIn(ApplicationUserEntity user, bool isPersistent)
        {
            AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
            var identity = _userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
            AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
        }

        private void AddErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

        private bool HasPassword()
        {
            var user = _userManager.FindById(User.Identity.GetUserId<int>());
            if (user != null)
            {
                return user.PasswordHash != null;
            }
            return false;
        }

        public enum ManageMessageId
        {
            ChangePasswordSuccess,
            SetPasswordSuccess,
            RemoveLoginSuccess,
            Error
        }

        private ActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }

            return RedirectToAction(MVC.Home.Index());
        }

        private class ChallengeResult : HttpUnauthorizedResult
        {
            public string LoginProvider { get; set; }

            public string RedirectUri { get; set; }

            public string UserId { get; set; }

            public ChallengeResult(string provider, string redirectUri) 
                : this(provider, redirectUri, null)
            {
            }

            public ChallengeResult(string provider, string redirectUri, string userId)
            {
                LoginProvider = provider;
                RedirectUri = redirectUri;
                UserId = userId;
            }

            public override void ExecuteResult(ControllerContext context)
            {
                var properties = new AuthenticationProperties()
                {
                    RedirectUri = RedirectUri
                };

                if (UserId != null)
                {
                    properties.Dictionary[XsrfKey] = UserId;
                }

                context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
            }
        }
        #endregion
    }
}